﻿using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.IO.Ports;
using System.Threading;
using System.Text.RegularExpressions;
using System.Timers;

namespace PortChat
{
    //Chat program uses a serial port to communicate
    public class PortChat
    {
        //true until user types "quit"
        private static bool _continue;
        //to communicate over a serial port
        private static SerialPort _serialPort;
        //port name
        private string portName;
        //port name number
        private int iPortNameNumber;
        //COM port name from sender binary format
        private string strComPortNameBin;
        //to do channel coding with strMessage
        private Encoder encoder = new Encoder();
        //for adding an error in the input string
        private ErrorGenerator errorGenerator = new ErrorGenerator();
        //for correcting an error in inpit string
        private ErrorCorrector errorCorrector = new ErrorCorrector();
        //for generating frames
        private FrameGenerator frameGenerator = new FrameGenerator();
        //for bitstuffing
        private BitStuffer bitStuffer = new BitStuffer();
        //for seperating frames
        private FrameSeparator frameSeparator = new FrameSeparator();
        //for analyzing a frame
        private FrameAnalyzer frameAnalyzer = new FrameAnalyzer();
        //a message costists of many digits
        private List<String> lstManyDigits = new List<String>();
        //for flow control
        private Boolean handshakeReceived = true;
        //sended frames
        private int iSendedFrames = 0;
        //for creating a checkword sender sided
        private CheckWordChecker checkWordCheckerSender = new CheckWordChecker();
        //for creating a checkword receiver sided
        private CheckWordChecker checkWordCheckerReceiver = new CheckWordChecker();
        //true, if there will be an error in the transmission
        private Boolean bThereWillBeAnError = true;
        //true, if an error will be recognized
        private Boolean bErrorWillBeRecognized = true;
        //true, if an error will be corrected
        private Boolean bErrorWillBeCorrected = true;
        //default error word
        private String strErrorWord = "00000001";
        //list of all codewords
        private List<String> lstCodeWordsReceived = new List<String>();

        //runs chatProg
        public void start()
        {
            //chat message
            string strMessage;
            //to recognize if user types "quit"
            StringComparer stringComparer = StringComparer.OrdinalIgnoreCase;
            //reading from the serial port is running now in an own thread
            Thread readThread = new Thread(Read);
            // Create a new SerialPort object with default settings.
            _serialPort = new SerialPort();
            _serialPort.PortName = SetPortName(_serialPort.PortName);
            _serialPort.BaudRate = _serialPort.BaudRate;
            _serialPort.Parity = _serialPort.Parity;
            _serialPort.DataBits = _serialPort.DataBits;
            _serialPort.StopBits = _serialPort.StopBits;
            _serialPort.Handshake = _serialPort.Handshake;
            // Set the read/write timeouts
            _serialPort.ReadTimeout = 500000;
            _serialPort.WriteTimeout = 500000;
            bThereWillBeAnError = showErrorQuestion();
            if (bThereWillBeAnError)            {
                strErrorWord = showInputErrorWord();
                bErrorWillBeRecognized = showRecognizeErrorQuestion();
                if (!bErrorWillBeRecognized)                
                {
                    bErrorWillBeCorrected = showCorrectErrorQuestion();
                }
                else
                {
                    bErrorWillBeCorrected = false;
                }
            }
            else
            {
                bErrorWillBeCorrected = false;
                bErrorWillBeRecognized = true;
            }
            _serialPort.Open();
            _continue = true;
            readThread.Start();
            Console.WriteLine("Type QUIT to exit");
            while (_continue)
            {
                strMessage = Console.ReadLine();
                //adding an $ is the \n-digit to mark the end of the message 
                strMessage = strMessage + "$";
                if (stringComparer.Equals("quit", strMessage))
                {
                    _continue = false;
                }
                else
                {
                    //a message with many digits
                    char[] charArrMessage = strMessage.ToCharArray();
                    foreach (char myChar2Send in charArrMessage)
                    {
                        if (handshakeReceived)
                        {
                            handshakeReceived = false;
                            //Console.WriteLine("handshake received");
                            //is send digit by digit
                            sendChar(myChar2Send);
                            //to wait until asynchron read thread sets handshakeReceived to true
                            System.Threading.Thread.Sleep(100); // 1 Sekunde warten
                            while (!handshakeReceived)
                            {
                                sendChar(myChar2Send);
                                System.Threading.Thread.Sleep(500); // 1 Sekunde warten
                            }                            
                        }                    
                    }
                }
            }
            readThread.Join();
            _serialPort.Close();
        }

        //should there be an error?
        public Boolean showErrorQuestion()
        {
            String strGenerateErrorReturn;
            Boolean bGenerateErrorReturn;
            Console.Write("Fehler erzeugen? (j/n): ");
            strGenerateErrorReturn = Console.ReadLine();
            bGenerateErrorReturn = strGenerateErrorReturn.Contains('j');
            return bGenerateErrorReturn;
        }

        //is the error going to be corrected?
        private bool showCorrectErrorQuestion()
        {
            String strCorrectErrorReturn;
            Boolean bCorrectErrorReturn;
            Console.Write("Fehler korrigieren? (j/n): ");
            strCorrectErrorReturn = Console.ReadLine();
            bCorrectErrorReturn = strCorrectErrorReturn.Contains('j');
            return bCorrectErrorReturn;
        }

        //is the error going to be recognized?
        private bool showRecognizeErrorQuestion()
        {
            String strRecognizeErrorReturn;
            Boolean bRecognizeErrorReturn;
            Console.Write("Fehler erkennen? (j/n): ");
            strRecognizeErrorReturn = Console.ReadLine();
            bRecognizeErrorReturn = strRecognizeErrorReturn.Contains('j');
            return bRecognizeErrorReturn;
        }

        //for inputting error word
        private String showInputErrorWord()
        {
            String strErrorWordReturn;
            Console.Write("Bitte 8Bit Fehlerwort eingeben (i.d.F. 00000001): ");
            strErrorWordReturn = Console.ReadLine();
            return strErrorWordReturn;
        }

        //reads a message from serial port
        public void Read()
        {
            while (_continue)
            {
                try
                {
                    string message = _serialPort.ReadLine();      
                    //if moore then one frame is send in a message
                    List<String> lstStuffedFrames = frameSeparator.separateFrames(message);                    
                    //List neccessary if moore then one frame is send in a message
                    List<String> lstFrames = bitStuffer.deStuff(lstStuffedFrames);
                    //analyze frame
                    //have to be a list, if realy moore then one frame is send in a list
                    //at the moment there is only one frame in list
                    String myFrame = null;
                    foreach (String myFrameL in lstFrames)
                    {
                        myFrame = myFrameL;
                        frameAnalyzer.analyzeFrame(myFrameL);                        
                    }          
                    //payload of frame
                    String strPayload = new String(frameAnalyzer.getPayload());
                    //handshake flag of frame
                    String strHandshakeFlag = new String(frameAnalyzer.getHandshakingFlag());;
                    //checkword flag of frame
                    String strCheckWordFlag = new String(frameAnalyzer.getCheckWordFlag());
                    //comport name of frame (binary format)
                    strComPortNameBin = new String(frameAnalyzer.getComPortName());                    
                    //frame is a non handshake frame (codeword or checkword frame)
                    if (strHandshakeFlag.Equals("0"))                    {
                        //sendHandShakeFrame();
                        //if it is a checkword frame
                        if (strCheckWordFlag.Equals("1"))
                        {
                            Console.WriteLine("Checkword frame");
                            //check word from checkWordCheckerReceiver
                            string strCheckWordReceiver = checkWordCheckerReceiver.getCheckWord();
                            //true, if transmission has no errors, otherwise false; in strPayload is the received checkword
                            Boolean transmissionWithoutErrors = (strCheckWordReceiver.Equals(strPayload));
                            //transmission has no errors
                            if (transmissionWithoutErrors)
                            {
                                sendHandShakeFrame();
                                Console.WriteLine("transmissionWithoutErrors");
                                checkWordCheckerReceiver.initializeCheckWord();
                                printLstOfCodeWordsReceived();

                            }
                            else //transmission has an error
                            {
                                //error will be corrected
                                if (bErrorWillBeCorrected)
                                {
                                    sendHandShakeFrame();
                                    lstCodeWordsReceived = correctError(strPayload, lstCodeWordsReceived);
                                    printLstOfCodeWordsReceived();
                                    checkWordCheckerReceiver.initializeCheckWord();
                                }
                                else // error will be recognised
                                {
                                    sendHandShakeFrame();
                                    printLstOfCodeWordsReceived();
                                    checkWordCheckerReceiver.initializeCheckWord();
                                }
                            }                            
                        } else // it is a word frame
                        {
                            Boolean bIsCodeWord = encoder.isCodeWord(strPayload);
                            if (bIsCodeWord)
                            {
                                sendHandShakeFrame();
                                checkWordCheckerReceiver.computeCheckWord(strPayload);
                                lstCodeWordsReceived.Add(strPayload);                                
                            }
                            else //error code word
                            {
                                if (bErrorWillBeRecognized)
                                {
                                    //sendHandShakeFrame();
                                    checkWordCheckerReceiver.computeCheckWord(strPayload);                                    
                                }
                                if (bErrorWillBeCorrected)
                                {
                                    sendHandShakeFrame();
                                    checkWordCheckerReceiver.computeCheckWord(strPayload);
                                    lstCodeWordsReceived.Add(strPayload);
                                }
                                if (!bErrorWillBeRecognized && !bErrorWillBeCorrected)
                                {
                                    sendHandShakeFrame();
                                    checkWordCheckerReceiver.computeCheckWord(strPayload);
                                    lstCodeWordsReceived.Add(strPayload);
                                }
                            }
                        }
                    }
                    else //if it is a handshake frame, set handshakeReceived = true, to allow sending further frames (flow control)
                    {
                        //Console.WriteLine("Handshake");
                        handshakeReceived = true;                        
                    } 
                }
                catch (TimeoutException) { }
            }
        }

        //chooses a correct port name
        public string SetPortName(string defaultPortName)
        {
            Boolean bPortNameExists = false;
            while (!bPortNameExists)
            {
                Console.WriteLine("Verfügbare Ports:");
                foreach (string s in SerialPort.GetPortNames())
                {
                    Console.WriteLine("   {0}", s);
                }
                Console.Write("COM Port eingeben: ", defaultPortName);
                portName = Console.ReadLine();
                foreach (string s in SerialPort.GetPortNames())
                {
                    if (portName.Contains(s))
                    {
                        bPortNameExists = true;
                    }
                }
                if (!bPortNameExists)
                {
                    Console.WriteLine("Eingegebener Port existert nicht!");
                }
            }
            iPortNameNumber = Convert.ToInt32(portName.Substring(3, 1));
            if (portName == "")
            {
                portName = defaultPortName;
            }
            return portName;
        }
                
        //delete the username from the massage to receive the pure message
        public String extractPureMessage(String strMessageP)
        {
            //remove username from message
            Regex rxGetMessage = new Regex("<.*?>:\\s");
            string[] strMessage = rxGetMessage.Split(strMessageP);
            return strMessage[1];

        }

        //insert the user name at the beginning of the pure message 
        public String insertUserName(String strPureMessageP)
        {
            return "<" + "COM" + bin2Dec(strComPortNameBin) + ">: " + strPureMessageP;
        }

        // encodes a message p.e.: <user1>: 000   ->   <user1>: a
        public String decodeMessage(String strCodeP)
        {
            String strCode = encoder.decode(strCodeP);
            return strCode;
        }
        //convert portNumberBin to a decimal format
        private String bin2Dec(String portNumber)
        {
            string portNumberDec = "0";
            if (portNumber.Equals("0000"))
            {
                portNumberDec = "0";
            }
            else if (portNumber.Equals("0001"))
            {
                portNumberDec = "1";
            }
            else if (portNumber.Equals("0010"))
            {
                portNumberDec = "2";
            }
            else if (portNumber.Equals("0011"))
            {
                portNumberDec = "3";
            }
            else if (portNumber.Equals("0100"))
            {
                portNumberDec = "4";
            }
            else if (portNumber.Equals("0101"))
            {
                portNumberDec = "5";
            }
            else if (portNumber.Equals("0110"))
            {
                portNumberDec = "6";
            }
            else if (portNumber.Equals("0111"))
            {
                portNumberDec = "7";
            }
            else if (portNumber.Equals("1000"))
            {
                portNumberDec = "8";
            }
            else if (portNumber.Equals("1001"))
            {
                portNumberDec = "9";
            }
            else
            {
                portNumberDec = "9";
            }
            return portNumberDec;
        }

        //send a handshake frame
        private void sendHandShakeFrame()
        {
            string strHandShakeFrame = frameGenerator.generateFrame("00000000", iPortNameNumber, true, false);
            String strFrameStuffed = bitStuffer.stuff(strHandShakeFrame);
            String strFrameWithStartEndFlag = frameGenerator.addStartEndFlag(strFrameStuffed);
            _serialPort.WriteLine(strFrameWithStartEndFlag);
        }

        //correct an error in the received codeword list if it is possible
        private List<String> correctError(String strCheckWordReceivedP, List<String> lstCodeWordsReceivedP)
        {
            //codeword which has an error
            String strCodeWordWithError = encoder.getCodeWordWithError(lstCodeWordsReceivedP);
            //index of error in codeword with error
            int iIndexOfError = getIndexOfErrorInCodeWordOfError(strCheckWordReceivedP, lstCodeWordsReceivedP);
            //if iIndexOfError == -1 multiple errors occurred, which can not be corrected
            if (iIndexOfError != -1)
            {
                //correct codeword
                String strCodeWordCorrected = errorCorrector.correctCodeWord(strCodeWordWithError, iIndexOfError);
                //the index of the errorword in codeword list
                int iIndexOfTheErrorwordInList = lstCodeWordsReceivedP.IndexOf(strCodeWordWithError);
                //remove errorword in codeword list
                lstCodeWordsReceivedP.RemoveAt(iIndexOfTheErrorwordInList);
                //insert corrected word in the codeword list
                lstCodeWordsReceivedP.Insert(iIndexOfTheErrorwordInList, strCodeWordCorrected);
                Console.WriteLine("transmission error corrected");
            }
            else
            {
                Console.WriteLine("Es traten Mehrfachfehler auf, die nicht korrigiert werden können.");
            }
            return lstCodeWordsReceivedP;
        }
        
        //print all codewords which were received
        private void printLstOfCodeWordsReceived()
        {
            String strCodeword = "";
            //process all saved payloads. they are now without errors
            foreach (String myCodeword in lstCodeWordsReceived)
            {
                strCodeword = decodeMessage(myCodeword);
                lstManyDigits.Add(strCodeword);
                string strManyDigits = "";
                //if the digit called "$" is received... 
                if (strCodeword.Equals("$"))
                {
                    //remove "$" from lstManyDigits
                    lstManyDigits.Remove("$");
                    //...all the collected digits will be connected to a string
                    foreach (String myDigit in lstManyDigits)
                    {
                        strManyDigits = strManyDigits + myDigit;
                    }
                    //the whole string will be printed
                    strManyDigits = insertUserName(strManyDigits);
                    Console.WriteLine(strManyDigits);
                    lstManyDigits.Clear();
                }
            }
            lstCodeWordsReceived.Clear();
        }

        //returns the position of the error of the codeword with error;
        //if return value is -1, then more than one failure is occurred;
        private int getIndexOfErrorInCodeWordOfError(String strCheckWordReceivedP, List<String> lstCodeWordsReceivedP)
        {
            //to compute the checkword which is different from the received checkword - cause the error in the codeword
            CheckWordChecker checkWordCheckerCorrecting = new CheckWordChecker();
            char[] charArrCheckWordReceived = strCheckWordReceivedP.ToCharArray();
            //compute checkword for the received codewords (including error) to get the difference between computed and received
            //checkword
            foreach (String myStrCodeWordReceived in lstCodeWordsReceivedP)
            {
                checkWordCheckerCorrecting.computeCheckWord(myStrCodeWordReceived);
            }
            //new computed checkword for received codewords (incluing error)
            String strCheckWordComputed = checkWordCheckerCorrecting.getCheckWord();
            //compare the received checkword with the computed checkword
            //the digit with the difference is the searched "indexOfError"
            int positionOfError = checkWordCheckerCorrecting.getDiffentDigitPosition(strCheckWordComputed, strCheckWordReceivedP);
            return positionOfError;
        }       

        //send the char
        private void sendChar(Char char2Send)
        {
            String strCode = encoder.encode(Convert.ToString(char2Send));
            //Console.WriteLine(strCode);
            checkWordCheckerSender.computeCheckWord(strCode);
            if (bThereWillBeAnError)
            {
                errorGenerator.setError(strErrorWord);
                strCode = errorGenerator.addError(strCode);
                bThereWillBeAnError = false;
            }
            String strFrame;
            strFrame = frameGenerator.generateFrame(strCode, iPortNameNumber, false, false);
            String strFrameStuffed;
            strFrameStuffed = bitStuffer.stuff(strFrame);
            String strFrameWithStartEndFlag;
            strFrameWithStartEndFlag = frameGenerator.addStartEndFlag(strFrameStuffed);
            _serialPort.WriteLine(strFrameWithStartEndFlag);
            iSendedFrames++;
            if ((iSendedFrames == 10) || (char2Send.Equals('$')))
            {
                strCode = checkWordCheckerSender.getCheckWord();
                checkWordCheckerSender.initializeCheckWord();
                strFrame = frameGenerator.generateFrame(strCode, iPortNameNumber, false, true);
                strFrameStuffed = bitStuffer.stuff(strFrame);
                strFrameWithStartEndFlag = frameGenerator.addStartEndFlag(strFrameStuffed);
                _serialPort.WriteLine(strFrameWithStartEndFlag);
                iSendedFrames = 0;
            }
        }        
    }
}